﻿using System;
using System.Drawing;
using System.Windows.Forms;
using NGridDrawer.Partitions;

namespace NGridDrawer.Draw
{
    /// <LastUpdate author="Blaise Braye" Date="15/06/2008 20:46"/>
    /// <summary>
    /// Provide the base class for the <see cref="IPartitionDrawer"/> interface 
    /// and enables <see cref="DataGridView"/> objects drawing
    /// </summary>
    public abstract class DataGridViewAbstractDrawer : IPartitionDrawer, IPropertiesDrawer
    {
        private ElementDrawnEventArgs _elementDrawingEventArgs;
        private ElementDrawnEventArgs _elementDrawnEventArgs;

        /// <summary>
        /// 	<para>Initializes an instance of the <see cref="DataGridViewAbstractDrawer"/> class.</para>
        /// </summary>
        /// <remarks>
        /// Initializes <see cref="PrintOrder"/> to <see cref="PartitionOrder"/>.OrderByLevelFirst by default
        /// </remarks>
        protected DataGridViewAbstractDrawer()
        {
            PrintOrder = PartitionOrder.OrderByLevelFirst;
            MustPrintColumnHeaders = true;
        }

        /// <summary>
        /// 	<para>Initializes an instance of the <see cref="DataGridViewAbstractDrawer"/> class.</para>
        /// </summary>
        /// <param name="gridView">Grid to paint</param>
        protected DataGridViewAbstractDrawer(DataGridView gridView)
            : this()
        {
            GridView = gridView;
        }

        /// <summary>
        /// Grid to paint
        /// </summary>
        protected DataGridView GridView { get; private set; }


        /// <summary>
        /// This is a mapping for the rows and columns of the grid which have to be painted
        /// </summary>
        public DataGridViewMapping Mapping { get; private set; }

        /// <summary>
        /// Gets/Sets the destination scale (useful to fit columns on one <see cref="PartitionColumn"/>)
        /// </summary>
        protected virtual float Scale { get; set; }

        #region IPartitionDrawer Members

        /// <summary>
        /// Useful to define if partitions must be horizontally centered in their bounds when painted
        /// </summary>
        public bool MustCenterHorizontally { get; set; }

        /// <summary>
        /// Useful to define if partitions must be vertically centered in their bounds when painted
        /// </summary>
        public bool MustCenterVertically { get; set; }


        /// <summary>
        /// Useful to indicates the order in which partitions have to be painted
        /// </summary>
        public PartitionOrder PrintOrder { get; set; }

        /// <summary>
        /// Calculate the columns header height of the <c>DataGridView</c></summary>
        /// <remarks>
        /// Used by the <c>BuildPartitions</c> method to minimize the clip height of a partition
        /// </remarks>
        /// <param name="graphic">graphics used to render the <c>DataGridView</c></param>
        /// <returns>The height of the columns header of the <c>DataGridView</c></returns>
        public float GetHeaderHeight(Graphics graphic)
        {
            return (MustPrintColumnHeaders) ? GetHeaderHeight_P(graphic) : 0;
        }

        /// <summary>
        /// Calculate the columns header height of the <c>DataGridView</c></summary>
        /// <remarks>
        /// Used by the <c>BuildPartitions</c> method to minimize the clip height of a partition
        /// </remarks>
        /// <param name="graphic">graphics used to render the <c>DataGridView</c></param>
        /// <returns>The height of the columns header of the <c>DataGridView</c></returns>
        protected abstract float GetHeaderHeight_P(Graphics graphic);

        #endregion

        #region IPropertiesDrawer Members

        /// <summary>
        /// Defines if only selected rows must be printed
        /// </summary>
        public bool MustPrintSelectedRows { get; set; }

        /// <summary>
        /// Defines if only selected columns must be printed
        /// </summary>
        public bool MustPrintSelectedColumns { get; set; }

        /// <summary>
        /// Defines if proposed sizes must be scaled in order to fit whole of columns on a single partition
        /// </summary>
        public bool MustFitColumnsToBounds { get; set; }


        /// <summary>
        /// Defines if row headers must be printed
        /// </summary>
        public bool MustPrintRowHeaders { get; set; }

        /// <summary>
        /// Defines if column headers must be printed
        /// </summary>
        public bool MustPrintColumnHeaders { get; set; }

        #endregion

        

        /// <summary>
        /// Paint a partition grid <paramref name="element"/> 
        /// starting from <paramref name="location"/> in the <paramref name="graphic"/> object
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="location">top left coordinate of the partition</param>
        /// <param name="element">partitionElement to paint</param>
        public virtual void PaintPartition(Graphics graphic, PointF location, PartitionElement element)
        {
            StartPrintingElement(graphic);

            var bounds = GetPartitionBounds(location, element);

            float currentY = MustPrintColumnHeaders 
                ? DrawColumnsHeader(graphic, bounds, element) 
                : bounds.Y;

            DrawRows(graphic, bounds, element, currentY);
        }

        /// <summary>
        /// Compute partition bounds
        /// </summary>
        /// <param name="location">indicates proposed start position of partition</param>
        /// <param name="element">partitionElement which must be painted</param>
        /// <returns>Partition bounds</returns>
        protected abstract RectangleF GetPartitionBounds(PointF location, PartitionElement element);


        /// <summary>Draw columns header of the <see cref="DataGridView"/>
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="bounds">
        /// Bounds area where the <paramref name="element"/> can be drawn
        /// </param>
        /// <param name="element">partitionElement to paint</param>
        /// <returns>
        /// Bottom Position of the Columns Header
        /// </returns>
        protected abstract float DrawColumnsHeader(Graphics graphic, RectangleF bounds, PartitionElement element);


        /// <summary>
        /// Draw the containment of the <paramref name="element"/> in the specified <paramref name="graphics"/>
        /// starting at <paramref name="currentY"/> y coordinate
        /// </summary>
        /// <param name="graphics">graphics used to paint</param>
        /// <param name="clipBounds">
        /// Bounds area where the <paramref name="element"/> can be drawn
        /// </param>
        /// <param name="element">partitionElement to paint</param>
        /// <param name="currentY">
        /// Top coordinate of the <paramref name="element"/> in <paramref name="graphics"/>
        /// </param>
        /// <returns>
        /// Bottom Position of the Partition element
        /// </returns>
        protected abstract float DrawRows(Graphics graphics, RectangleF clipBounds, PartitionElement element, float currentY);

        /// <summary>
        /// Calculate and initialize the Scale to use on the target graphic
        /// </summary>
        /// <param name="graphic">This is the graphics used to paint</param>
        /// <param name="clipWidth">the clip width of a partition element</param>
        protected void InitializeScale(Graphics graphic, float clipWidth)
        {
            Scale = 1;

            if (!MustFitColumnsToBounds) return;
            
            // calculate the width of the grid in our destination wourld
            float gridWidth = (MustPrintRowHeaders) ? RowHeaderWidth(graphic) : 0;
            for (int i = 0; i < Mapping.ColumnCount; i++)
                gridWidth += ColumnWidth(graphic, i);

            // if clipWidth is lower that grid width, we have to minimize the width of the grid
            // we do that by scaling  from calculated grid width to the clip width
            if (clipWidth < gridWidth)
                Scale = clipWidth/gridWidth;
        }


        private void initializeMapping()
        {
            Scale = 1;
            // prepare interesting rows in the grid (visible and/or selected)
            DataGridViewElementStates rowsStates = DataGridViewElementStates.Visible;
            if (MustPrintSelectedRows)
                rowsStates |= DataGridViewElementStates.Selected;


            var rowsMap = new int[GridView.Rows.GetRowCount(rowsStates)];

            if (rowsMap.Length == 0)
                throw new InvalidOperationException("No selected rows found");

            // store the indexes of interesting rows in a table which will be used as a rows mapping
            int row = GridView.Rows.GetFirstRow(rowsStates);
            for (int i = 0; i < rowsMap.Length; i++)
            {
                rowsMap[i] = row;
                row = GridView.Rows.GetNextRow(row, rowsStates);
            }

            // prepare interesting columns in the grid (visible and/or selected)
            DataGridViewElementStates colsStates = DataGridViewElementStates.Visible;
            if (MustPrintSelectedColumns)
                colsStates |= DataGridViewElementStates.Selected;

            var columnsMap = new int[GridView.Columns.GetColumnCount(colsStates)];

            if (columnsMap.Length == 0)
                throw new InvalidOperationException("No selected columns found");

            // store the indexes of interesting columns in a table which will be used as a columns mapping
            DataGridViewColumn column = GridView.Columns.GetFirstColumn(colsStates);

            for (int i = 0; i < columnsMap.Length && column != null; i++)
            {
                columnsMap[i] = column.Index;
                column = GridView.Columns.GetNextColumn(column, colsStates, DataGridViewElementStates.None);
            }

            // initialize the mapping
            Mapping = new DataGridViewMapping(GridView, rowsMap, columnsMap);
        }


        private void fixClipHeights(Graphics graphic, float[] clipHeights)
        {
            if (MustPrintColumnHeaders)
            {

                float headerHeight = GetHeaderHeight(graphic);
                for (int i = 0; i < clipHeights.Length; i++)
                {
                    clipHeights[i] -= headerHeight;
                }
            }
        }

        #region IPartitionDrawer members

        /// <summary>
        /// Get the width of a column
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="columnKey">index of the interesting column</param>
        /// <returns>float value, width of the interesting column</returns>
        public abstract float ColumnWidth(Graphics graphic, int columnKey);

        /// <summary>
        /// Get the width of a [row header] column
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <returns>float value, width applied the row headers</returns>
        public float RowHeaderWidth(Graphics graphic)
        {
            return MustPrintRowHeaders ? RowHeaderWidth_P(graphic) : 0;
        }


        /// <summary>
        /// Get the width of a [row header] column
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <returns>float value, width applied the row headers</returns>
        public abstract float RowHeaderWidth_P(Graphics graphic);

        /// <summary>
        /// Get the height of a row
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="rowKey">index of the interesting row</param>
        /// <returns>float value, height of the interesting row</returns>
        public abstract float RowHeight(Graphics graphic, int rowKey);


        /// <summary>
        /// get the number of rows which will be painted
        /// </summary>
        public int RowCount
        {
            get { return Mapping.RowCount; }
        }

        /// <summary>
        /// get the number of columns which will be painted
        /// </summary>
        public int ColumnCount
        {
            get { return Mapping.ColumnCount; }
        }

        /// <summary>
        /// Partitions the grid of data into printable areas.
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="clipHeights">defines the maximum bounds for height of each partitionLevel</param>
        /// <param name="clipWidths">defines the maximum bounds for width of each partitionColumn</param>
        /// <returns>A partition of the grid to paint</returns>
        /// <seealso cref="NGridDrawer.Partitions.PartitionGrid"/>
        public virtual PartitionGrid BuildPartition(
            Graphics graphic, float[] clipHeights, float[] clipWidths)
        {
            InitializeDrawerForGrid(graphic, clipHeights, clipWidths);

            //build partitions
            return BuildPartition_P(graphic, clipHeights, clipWidths);
        }

        /// <summary>
        /// Partitions the grid of data into printable areas.
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="clipHeights">defines the maximum bounds for height of each partitionLevel</param>
        /// <param name="clipWidths">defines the maximum bounds for width of each partitionColumn</param>
        /// <returns>A partition of the grid to paint</returns>
        /// <seealso cref="NGridDrawer.Partitions.PartitionGrid"/>
        protected virtual PartitionGrid BuildPartition_P(Graphics graphic, float[] clipHeights, float[] clipWidths)
        {
            return PartitionBuilder.BuildPartition(graphic, clipHeights, clipWidths, PrintOrder, this);
        }


        ///<summary>
        /// Event raised when a header cell is gonna be drawn
        ///</summary>
        public event EventHandler<DataGridViewCellPaintingEventArgs> HeaderCellDrawing;

        ///<summary>
        /// Event raised when an element is gonna be drawn
        ///</summary>
        public event EventHandler<ElementDrawnEventArgs> ElementDrawing;


        ///<summary>
        /// Event raised when an element has been drawn
        ///</summary>
        public event EventHandler<ElementDrawnEventArgs> ElementDrawn;

        /// <summary>
        /// Draw the <paramref name="element"/> in the <paramref name="graphic"/>
        /// with <paramref name="clipBounds"/>.Location as a top left coordinate
        /// </summary>
        /// <param name="graphic">This is the graphics used to paint</param>
        /// <param name="clipBounds">This should be the maximum bounds of the element</param>
        /// <param name="element">This is the partition to paint</param>
        public virtual void Paint(Graphics graphic, RectangleF clipBounds, PartitionElement element)
        {
            PointF topleft = clipBounds.Location;

            if (MustCenterHorizontally)
            {
                topleft.X +=
                    (clipBounds.Width - element.Size.Width)/2F;
            }

            if (MustCenterVertically)
            {
                topleft.Y +=
                    (clipBounds.Height - element.Size.Height)/2F;
            }

            PaintPartition(graphic, topleft, element);
        }

        /// <summary>
        /// this method must be called when an element has been drawn
        /// </summary>
        /// <param name="sender">element drawn</param>
        /// <param name="bounds">paint area of the element drawn</param>
        protected void OnElementDrawn(object sender, RectangleF bounds)
        {
            _elementDrawnEventArgs.Bounds = bounds;

            if (ElementDrawn != null) ElementDrawn(sender, _elementDrawnEventArgs);
        }

        /// <summary>
        /// this method must be called when an element is being drawn
        /// </summary>
        /// <param name="sender">element being drawn</param>
        /// <param name="bounds">paint area where the element will be drawn</param>
        protected void OnElementDrawing(object sender, RectangleF bounds)
        {
            _elementDrawingEventArgs.Bounds = bounds;
            if (ElementDrawing != null) ElementDrawing(sender, _elementDrawingEventArgs);
        }



        


        /// <summary>
        /// Initialize this drawer to print an element in the <paramref name="graphic"/>
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        protected void StartPrintingElement(Graphics graphic)
        {
            _elementDrawnEventArgs = new ElementDrawnEventArgs(graphic);
            _elementDrawingEventArgs = new ElementDrawnEventArgs(graphic);
        }

        /// <summary>
        /// Initialize the <see cref="IPartitionDrawer"/> instance in order to build a partition
        /// </summary>
        /// <param name="graphic">graphics used to paint</param>
        /// <param name="clipHeights">defines the maximum bounds for height of each partitionLevel</param>
        /// <param name="clipWidths">defines the maximum bounds for width of each partitionColumn</param>
        /// <remarks>
        /// generally, this method is the first call in <see cref="IPartitionDrawer.BuildPartition"/> method.
        /// It is public only because we could need to initialize this to get some information in the grid
        /// without having to build its partition.
        /// </remarks>
        protected virtual void InitializeDrawerForGrid(Graphics graphic, float[] clipHeights, float[] clipWidths)
        {
            initializeMapping();
            InitializeScale(graphic, clipWidths[0]);

            fixClipHeights(graphic, clipHeights);
        }

        #endregion

    }
}